﻿using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml;
using GX;

public class VersionTool
{
	/// <summary>
	/// 将要打包的资源根目录
	/// </summary>
	public static readonly string InputHome = @"Assets/Resources/";
	/// <summary>
	/// 输出版本内容的根目录
	/// </summary>
	public static readonly string OutputHome = Path.Combine(Application.dataPath, "../AssetBundle");
	/// <summary>
	/// 版本输出的根目录
	/// </summary>
	public static readonly string VersionHome = Path.Combine(Application.dataPath, "../Version");

#if UNITY_5
	#region VersionTool/Resouces -> AssetBundle
	[MenuItem("VersionTool/Resouces -> AssetBundle")]
	static void Resouces2AssetBundle5()
	{
		var watch = new System.Diagnostics.Stopwatch();
		watch.Start();
		var target = Path.Combine(OutputHome, EditorUserBuildSettings.activeBuildTarget.ToString()); // 目标目录

		var option = BuildAssetBundleOptions.None
			| BuildAssetBundleOptions.AppendHashToAssetBundleName
			| BuildAssetBundleOptions.DeterministicAssetBundle;

		// 要打包的文件列表
		AssetDatabase.Refresh();
		var builds = (
			from f in AssetDatabase.GetAllAssetPaths().Where(i => i.StartsWith(InputHome))
			where File.Exists(f) // 过滤掉目录
			select new AssetBundleBuild()
			{
				assetNames = new[] { f },
				assetBundleName = AssetDatabase.AssetPathToGUID(f) + ".ab",
			}).ToArray();

		// 打包
		Directory.CreateDirectory(target);
		var manifest = BuildPipeline.BuildAssetBundles(target, builds, option, EditorUserBuildSettings.activeBuildTarget);

		// flist生成
		var flist = new FList(
			from b in builds
			from ab in manifest.GetAllAssetBundles()
			where b.assetBundleName.Substring(0, b.assetBundleName.IndexOf('.')) == ab.Substring(0, ab.IndexOf('_'))
			select new FList.FileItem
			{
				path = b.assetNames.First().Substring(InputHome.Length), // 只要资源的有效相对路径
				ab = ab,
				size = new FileInfo(Path.Combine(target, ab)).Length,
				depends = manifest.GetAllDependencies(ab)
			})
		{ Target = EditorUserBuildSettings.activeBuildTarget.ToString() };
		if (flist.Count != builds.Count())
			Debug.LogError("Match AssetBundleName error.");
		flist.Save(target + ".xml");

		watch.Stop();
		var log = string.Format("Resouces -> AssetBundle OK.\ntarget={0}\ntime={1}",
			EditorUserBuildSettings.activeBuildTarget,
			watch.Elapsed);
		Debug.Log(log);
		EditorUtility.DisplayDialog("Message", log, "OK");
	}
	#endregion

	#region VersionTool/AssetBundle -> Version
	[MenuItem("VersionTool/AssetBundle -> Version")]
	static void AssetBundle2Version()
	{
		var target = EditorUserBuildSettings.activeBuildTarget.ToString();
		var flist = FList.Load(Path.Combine(OutputHome, target + ".xml"));
		if (flist == null || flist.Target != target)
		{
			Resouces2AssetBundle5();
			flist = FList.Load(Path.Combine(OutputHome, target + ".xml"));
		}

		var flistBase = FList.Load(Path.Combine(VersionHome, "base/" + target + ".xml"));
		if (flistBase == null) // 制作全版本
		{
			// base 生成
			{
				var dst = Path.Combine(VersionHome, "base/" + target);
				if (Directory.Exists(dst))
					Directory.Delete(dst, true);
				Directory.CreateDirectory(dst);
				foreach (var f in flist)
				{
					File.Copy(Path.Combine(OutputHome, target + "/" + f.ab), Path.Combine(dst, f.ab));
				}
				flist.Save(dst + ".xml");
			}
			// patch 生成
			{
				var dst = Path.Combine(VersionHome, "patch/" + target);
				if (Directory.Exists(dst))
					Directory.Delete(dst, true);
				Directory.CreateDirectory(dst);
				new FList() { Target = target }.Save(dst + ".xml");
			}
			Debug.Log("OK base");
		}
		else // 制作补丁版本
		{
			var dst = Path.Combine(VersionHome, "patch/" + target);
			if (Directory.Exists(dst))
				Directory.Delete(dst, true);
			Directory.CreateDirectory(dst);
			var flistPatch = new FList(flist.Except(flistBase)) { Target = target };
			foreach (var f in flistPatch)
			{
				File.Copy(Path.Combine(OutputHome, target + "/" + f.ab), Path.Combine(dst, f.ab));
			}
			flistPatch.Save(dst + ".xml");
			Debug.Log("OK patch");
		}
	}

	#endregion
#endif

	#region VersionTool/Resouces -> AssetBundle for Unity-4.x
#if !UNITY_5
	[MenuItem("VersionTool/Resouces -> AssetBundle")]
#endif
	static void Resouces2AssetBundle()
	{
		var watch = new System.Diagnostics.Stopwatch();
		watch.Start();
		var buildTimes = 0; // BuildAssetBundle调用次数，用于做版本的监控调优

		var option = BuildAssetBundleOptions.None
			| BuildAssetBundleOptions.CollectDependencies
			| BuildAssetBundleOptions.CompleteAssets
			| BuildAssetBundleOptions.UncompressedAssetBundle // 采用unity进行压缩非常慢，很影响做版本的效率
			| BuildAssetBundleOptions.DeterministicAssetBundle;

		var temp = Path.Combine(OutputHome, "temp"); // 原始目录结构
		var archive = Path.Combine(OutputHome, "archive"); // hash文件系统

		Directory.CreateDirectory(OutputHome);
		if (Directory.Exists(temp))
			Directory.Delete(temp, true);
		AssetDatabase.Refresh();
		var items = GetFileItems(InputHome).ToDictionary(i => i.Path);

		#region BuildAssetBundle
		//if(false)
		{
			// 缓存加载
			var cache = new FileItemCache();
			cache.Deserialize(Path.Combine(OutputHome, "cache.xml"));

			#region 工作目录创建
			foreach (var i in items.Keys)
			{
				Directory.CreateDirectory(Path.GetDirectoryName(Path.Combine(temp, i)));
			}
			for (var i = 0x00; i <= 0xFF; i++)
			{
				Directory.CreateDirectory(Path.Combine(archive, i.ToString("x2")));
			}
			#endregion

			try
			{
				foreach (var i in items.Values)
				{
					var nodes = i.DependsAll(items).Concat(i).ToList();
					// 缓存检测
					var ca = cache[nodes];
					if (ca != null)
					{
						var hash = ca.Item1;
						var exist = new FileInfo(GetHashFilePath(archive, hash));
						if (exist.Exists && exist.Length == ca.Item2)
						{
							i.OutputHash = hash;
							i.OutputSize = ca.Item2;
							continue;
						}
					}

					foreach (var n in nodes)
					{
						buildTimes++;
						BuildPipeline.PushAssetDependencies();
						BuildPipeline.BuildAssetBundle(n.Assets[0], n.Assets, Path.Combine(temp, n.Path), option, EditorUserBuildSettings.activeBuildTarget);
					}
					for (var n = 0; n < nodes.Count; n++)
					{
						BuildPipeline.PopAssetDependencies();
					}

					// 计算MD5和文件大小
					var bytes = File.ReadAllBytes(Path.Combine(temp, i.Path));
					i.OutputSize = bytes.Length;
					i.OutputHash = GX.MD5.ComputeHashString(bytes);

					// 生成hash文件系统
					var dst = GetHashFilePath(archive, i.OutputHash);
					if (File.Exists(dst) == false)
						File.Move(Path.Combine(temp, i.Path), dst);

					// 缓存更新
					cache[nodes] = Tuple.Create(i.OutputHash, i.OutputSize);

					System.GC.Collect();
				}
			}
			finally
			{
				// 缓存保存
				cache.Serialize(Path.Combine(OutputHome, "cache.xml"));
			}
		}
		#endregion

		#region flist生成
		// 生成flist节点
		var flist = new XDocument(new XElement("files",
			new XAttribute("target", EditorUserBuildSettings.activeBuildTarget),
			from i in items.Values.OrderBy(i => i.Path, new GX.NaturalPathComparer()) // flist采用自然排序
			select new XElement("file",
				new XAttribute("path", items[i.Path].Path),
				new XAttribute("hash", items[i.Path].OutputHash),
				new XAttribute("size", items[i.Path].OutputSize),
				from d in i.Depends
				select new XElement("file",
					//new XAttribute("hash", items[d].hash),
					new XAttribute("path", items[d].Path)))));
		flist.Save(Path.Combine(OutputHome, "flist.xml"), new XmlWriterSettings()
		{
			Indent = true,
			IndentChars = "\t",
			Encoding = System.Text.Encoding.UTF8,
		});
		#endregion

		Directory.Delete(temp, true);
		AssetDatabase.Refresh();

		watch.Stop();
		var log = string.Format("Resouces -> AssetBundle OK target={0} build={1}/{2} time={3}",
			EditorUserBuildSettings.activeBuildTarget,
			buildTimes, items.Count,
			watch.Elapsed);
		Debug.Log(log);
		EditorUtility.DisplayDialog("Message", log, "OK");
	}

	#region class FileItemCache
	class FileItemCache : IXmlSerializable
	{
		public Dictionary<string, Tuple<string, int>> cache = new Dictionary<string, Tuple<string, int>>();

		public Tuple<string, int> this[IEnumerable<FileItem> chain]
		{
			get
			{
				Tuple<string, int> value;
				return cache.TryGetValue(GetKey(chain), out value) ? value : null;
			}
			set
			{
				cache[GetKey(chain)] = value;
			}
		}

		private static string GetKey(IEnumerable<FileItem> chain)
		{
			var separator = " ";
			return EditorUserBuildSettings.activeBuildTarget + separator +
				string.Join(separator, (from i in chain select i.InputHash).ToArray());
		}

		public void Serialize(string file)
		{
			var xml = new XDocument(this.Serialize());
			xml.Save(file, new XmlWriterSettings()
			{
				Indent = true,
				IndentChars = "\t",
			});
		}

		public void Deserialize(string file)
		{
			try
			{
				var xml = XDocument.Load(file);
				this.Deserialize(xml.Root);
			}
			catch { }
		}

		#region IXmlSerializable 成员

		public XElement Serialize()
		{
			return new XElement("cache",
				from i in cache
				select new XElement("item",
					new XAttribute("path", i.Key),
					new XAttribute("hash", i.Value.Item1),
					new XAttribute("size", i.Value.Item2)));
		}

		public void Deserialize(XElement xml)
		{
			cache.Clear();
			foreach (var i in xml.Elements("item"))
			{
				cache[i.AttributeValue("path")] = Tuple.Create(i.AttributeValue("hash"), int.Parse(i.AttributeValue("size")));
			}
		}

		#endregion
	}
	#endregion

	#region class FileItem
	class FileItem
	{
		private FileItem() { }
		public string Path { get; private set; }
		public Object[] Assets { get; private set; }
		public string[] Depends { get; private set; }
		public string InputPath { get; private set; }
		public string InputHash { get; private set; }
		/// <summary>
		/// 打包后生成文件的md5
		/// </summary>
		public string OutputHash { get; set; }
		/// <summary>
		/// 打包后生成文件的大小
		/// </summary>
		public int OutputSize { get; set; }

		public static FileItem Create(string baseDir, string path)
		{
			var assets = AssetDatabase.LoadAllAssetsAtPath(path);
			var item = new FileItem();
			item.InputPath = path;
			item.InputHash = MD5.ComputeHashStringFromFile(path);
			item.Path = path.Substring(baseDir.Length); // “绝对资源路径”处理为相对路径
			item.Assets = assets;
			item.Depends = (
				from i in AssetDatabase.GetDependencies(new string[] { path })
				where i != path && i.StartsWith(baseDir)
				select i.Substring(baseDir.Length)).ToArray(); // “绝对资源路径”处理为相对路径
			return item;
		}

		/// <summary>
		/// 递归得到所有依赖资源
		/// </summary>
		/// <param name="dic"></param>
		/// <returns></returns>
		public IEnumerable<FileItem> DependsAll(IDictionary<string, FileItem> dic)
		{
			return TopologicalSort(DependsRecursive(dic).Distinct());
		}
		private IEnumerable<FileItem> DependsRecursive(IDictionary<string, FileItem> dic)
		{
			foreach (var d in Depends)
			{
				foreach (var s in dic[d].DependsRecursive(dic))
					yield return s;
				yield return dic[d];
			}
		}
	}
	#endregion

	static string GetHashFilePath(string pathBase, string hash)
	{
		return Path.Combine(pathBase, hash.Substring(0, 2) + "/" + hash + ".unity3d");
	}
	/// <summary>
	/// 得到给定目录下所有的资源列表
	/// </summary>
	/// <param name="dir"></param>
	/// <returns></returns>
	static IEnumerable<FileItem> GetFileItems(string dir)
	{
		return
			from src in AssetDatabase.GetAllAssetPaths().Where(i => i.StartsWith(dir))
			where Directory.Exists(src) == false // 过滤掉目录
			let item = FileItem.Create(dir, src)
			where item != null // 过滤掉无效资源
			select item;
	}

	/// <summary>
	/// 根据资源列表的依赖关系进行拓扑排序
	/// </summary>
	/// <param name="items"></param>
	/// <returns></returns>
	static IEnumerable<FileItem> TopologicalSort(IEnumerable<FileItem> items)
	{
		var list = items.Select(i => Tuple.Create(i, i.Depends.ToList())).ToList();

		// 按照拓扑排序返回
		while (list.Any())
		{
			// 返回前驱最少的所有“叶节点”
			var min = list.Min(i => i.Item2.Count);
			var leaf = list.Where(i => i.Item2.Count == min).ToList();
			list.RemoveAll(i => leaf.Contains(i));
			foreach (var i in leaf)
				yield return i.Item1;

			// 剩余节点依次删除对叶节点的依赖
			foreach (var n in list)
				n.Item2.RemoveAll(i => leaf.Any(z => z.Item1.Path == i));
		}
	}
	#endregion
}
